function [taskTree, taskList] = configureDots2afcTask
%Configure a dots kinetogram 2-alternative forced choice task
%
%   [taskTree, taskList] = configureDots2afcTask
%
%   taskTree is a topsBlockTree object which organizes tasks and trials.
%   taskTree.run() will start the task.  taskTree.gui() will launch a
%   graphical interface for viewing the organization of the task.
%
%   taskList is a topsGroupedList object which holds all the objects and
%   data needed to run the task, including taskTree.  taskList.gui() will
%   launch a graphical interface for viewing the objects and data.
%
%   The task uses graphical stimuli including targets, a fixation point,
%   and a random dot kinetogram.  The subject is expected to decide towards
%   which target the dots in the kinetogram appeared to be moving.
%
%   The task uses a HID gamepad to take input.  The subject may highlight
%   one target at a time using the gamepad's x and y axis controls.  The
%   subject may indicate a final decision by pressing the gamepad's primary
%   button.
%
%   The task contains multiple trials, each containing one stimulus
%   presentation and one decision.  Generally, the trials follow the same
%   sequence:
%       - the fixation point appears on the blank screen
%       - there is a "preparation" time interval
%       - the dots kinetogram appears on the screen
%       - there is a "stimulus" time interval:
%           - the kinetogram presents a motion signal
%           .
%       - the dots kinetogram is replaced on the screen by two gray targets
%       - there is a "choice" time interval:
%           - the subject may select a target
%           .
%       - the target matching the kinetogram's motion signal turns green,
%       the other target turns red
%       - there is a short "feedback" interval
%       - the screen goes blank
%       - the trial ends and an intertrial interval follows
%       .
%
%   There are three variations on this trial sequence, each with different
%   timing behavior.  The three trial types are:
%       - Fixed Time
%           - the "preparation" interval is constant
%           - the "stimulus" interval is constant
%           - the "choice" interval is constant
%           .
%       - Reaction Time
%           - the "preparation" interval is constant
%           - the "stimulus" interval lasts until the subject moves an axis
%           control on the gamepad
%           - the "choice" interval lasts until the subject presses the
%           primary button on the gamepad.
%           .
%       - Preparation Time
%           - the "preparation" interval lasts until the subject presses
%           the primary button on the gamepad.
%           - the "stimulus" interval is constant
%           - the "choice" interval is constant
%           .
%       .
%   The "feedback" interval is constant for all trial types.
%
%   Task stimulus and task data are logged with topsDataLog.  Data about
%   the flow of events during the task are also logged.
%
%   Task parameters, such as the lengths of constant time intervals, may be
%   edited near the top of this file, configureDots2afcTask.m.
%
% 2010 benjamin.heasly@gmail.com
%   Seattle, WA

% @todo it would be worth identifying and documenting my mnemonic puns
%   - "leftward" and "rightward" as query classifications and degree values
%   - query classifications like "input" as state names
%   - names properties of objects
%   - these should also be specified as taskList constants and/or local
%   variables that get defined once

%% Organization:
% a container for all task data and objects
%   partitioned into arbitrary groups
%   viewable with taskList.gui
taskList = topsGroupedList;

% give the task a name, used below
taskName = 'dots2afc';


%% Constants:
% store some constants in the taskList container
%   used during configuration
%   used while task is running
taskList{'timing'}{'preparation'} = 1;
taskList{'timing'}{'stimulus'} = 1;
taskList{'timing'}{'choice'} = 2;
taskList{'timing'}{'feedback'} = 1;
taskList{'timing'}{'trial timeout'} = 600;
taskList{'timing'}{'intertrial'} = .25;

taskList{'graphics'}{'white'} = [255 255 255];
taskList{'graphics'}{'gray'} = [128 128 128];
taskList{'graphics'}{'red'} = [196 64 32];
taskList{'graphics'}{'green'} = [64 196 32];
taskList{'graphics'}{'stimulus diameter'} = 5;
taskList{'graphics'}{'fixation pixels'} = 10;
taskList{'graphics'}{'leftward'} = 180;
taskList{'graphics'}{'rightward'} = 0;
taskList{'graphics'}{'left side'} = -5;
taskList{'graphics'}{'right side'} = 5;
taskList{'graphics'}{'neutral pixels'} = [5, 5];
taskList{'graphics'}{'leftward pixels'} = [30, 5];
taskList{'graphics'}{'rightward pixels'} = [5, 30];

taskList{'control'}{'trials per block'} = 5;
taskList{'control'}{'blocks per session'} = 1;
taskList{'control'}{'coin toss'} = 0;

%% Graphics:
% create some graphics objects
%   configure them using constants from above
%   store them in the taskList container
dm = dotsTheDrawablesManager.theObject;

% a fixation point
fp = dm.newObject('dotsDrawableTargets');
fp.color = taskList{'graphics'}{'gray'};
fp.dotSize = taskList{'graphics'}{'fixation pixels'};
taskList{'graphics'}{'fixation point'} = fp;

% target dots
targs = dm.newObject('dotsDrawableTargets');
targs.color = taskList{'graphics'}{'gray'};
targs.dotSize = taskList{'graphics'}{'neutral pixels'};
left = taskList{'graphics'}{'left side'};
right = taskList{'graphics'}{'right side'};
targs.x = [left, right];
targs.y = [0 0];
targs.isVisible = false;
taskList{'graphics'}{'targets'} = targs;

% a random dots stimulus
stim = dm.newObject('dotsDrawableDotKinetogram');
stim.color = taskList{'graphics'}{'white'};
stim.dotSize = 3;
stim.diameter = taskList{'graphics'}{'stimulus diameter'};
stim.isVisible = false;
taskList{'graphics'}{'stimulus'} = stim;

% add all objects to one group, to be drawn simultaneously
dm.addObjectToGroup(fp, taskName);
dm.addObjectToGroup(targs, taskName);
dm.addObjectToGroup(stim, taskName);
dm.activeGroup = taskName;

%% Input:
% create an input object, a gamepad
%   configure it to classify inputs as needed for this task
%   store it in the taskList container
qm = dotsTheQueryablesManager.theObject;
gp = qm.newObject('dotsQueryableHIDGamepad');
taskList{'input'}{'gamepad'} = gp;
if gp.isAvailable
    % identify axes movement phenomenons
    %   see gp.phenomenons.gui()
    left = gp.phenomenons{'axes'}{'min_X'};
    right = gp.phenomenons{'axes'}{'max_X'};
    anyAxis = gp.phenomenons{'axes'}{'any_axis'};
    
    % identify primary button press phenomenon
    %   see gp.phenomenons.gui()
    pressed = gp.phenomenons{'pressed'}{'pressed_button_1'};
    
    % classify phenomenons to produce arbitrary outputs
    %   each output will match a state name, below
    gp.addClassificationInGroupWithRank(left, 'leftward', taskName, 2);
    gp.addClassificationInGroupWithRank(right, 'rightward', taskName, 3);
    gp.addClassificationInGroupWithRank(pressed, 'commitment', taskName, 4);
    gp.activeClassificationGroup = taskName;
end

%% Control:
% create three types of control objects:
%       - topsBlockTree organizes flow outside of trials
%       - topsStatelist organizes flow within trials
%       - topsFunctionLoop organizes moment-to-moment, concurrent behaviors
%       within trials
%   connect these to each other
%   store them in the taskList container

% the trunk of the tree, branches are added below
taskTree = topsBlockTree;
taskTree.name = taskName;
taskTree.iterations = taskList{'control'}{'blocks per session'};
taskTree.blockStartFevalable = {@openScreenWindow, dm};
taskTree.blockEndFevalable = {@closeScreenWindow, dm};

% one loop object can accomodate all the trial types below
taskLoop = topsFunctionLoop;
taskLoop.addFunctionToGroupWithRank({@step, dm}, taskName, 0);
taskLoop.addFunctionToGroupWithRank({@readData, gp}, taskName, 1);
taskLoop.addFunctionToGroupWithRank({@mayDrawNextFrame, dm}, taskName, 10);
taskList{'control'}{'task loop'} = taskLoop;

% each trial type gets its own state machine block tree "branch"
%   state machines share many states

%% Fixed Time
fixedTimeMachine = topsStateMachine;
fixedTime = 'fixed time';
fixedTimeMachine.name = fixedTime;

prepTime = taskList{'timing'}{'preparation'};
stimTime = taskList{'timing'}{'stimulus'};
choiceTime = taskList{'timing'}{'choice'};
feedTime = taskList{'timing'}{'feedback'};
trialTime = taskList{'timing'}{'trial timeout'};

feedback = {@drawCorrectIncorrect, taskList};
ackLeft = {@acknowledgeLeft, taskList};
ackRight = {@acknowledgeRight, taskList};

fixedTimeStates = { ...
    'name',     'entry',        'timeout',	'input',	'exit',         'next'; ...
    'preparation',{@show,fp},   prepTime,   {},         {},             'stimulus'; ...
    'stimulus', {@show,stim},   stimTime,   {},         {@hide,stim},   'choice'; ...
    'choice',   {@show,targs},  choiceTime, {@queryNextTracked,gp},{},  'feedback'; ...
    'leftward', ackLeft,        0,          {},         {},             'responded'; ...
    'rightward',ackRight,       0,          {},         {},             'responded'; ...
    'responded',{},             0,          {},         {@hide,fp},     'feedback'; ...
    'feedback', feedback,       feedTime,   {},         {@hide,targs},  ''; ...
    'unavailable',{},           0,          {},         {@hide,fp},     'feedback'; ...
    };
fixedTimeMachine.addMultipleStates(fixedTimeStates);
taskList{'control'}{'fixed time machine'} = fixedTimeMachine;

% add a group to the loop object to accomodate the Fixed Time trials
taskLoop.addFunctionToGroupWithRank({@step, fixedTimeMachine}, fixedTime, 5);
taskLoop.mergeGroupsIntoGroup({taskName, fixedTime}, fixedTime);

% add a branch to the tree trunk to lauch a Fixed Time trial
fixedTimeTree = topsBlockTree;
fixedTimeTree.name = fixedTime;
fixedTimeTree.iterations = taskList{'control'}{'trials per block'};
fixedTimeTree.blockActionFevalable = {@runTrial, taskList, fixedTimeMachine};
taskTree.addChild(fixedTimeTree);

%% Reaction Time
reactionTimeMachine = topsStateMachine;
reactionTime = 'reaction time';
reactionTimeMachine.name = reactionTime;

% only need to modify states from Fixed Time
%   respond to input during stimulus
%   wait for a button press commitment
reactionTimeStates = { ...
    'name',     'entry',        'timeout',	'input',	'exit',         'next'; ...
    'stimulus', {@show,stim},	trialTime,	{@queryNextTracked,gp},{@hide,stim},   'feedback'; ...
    'responded',{@show,targs},	trialTime,	{@queryNextTracked,gp},{@hide,fp},     'feedback'; ...
    'commitment',{},           	0,          {},         {@hide,fp},     'feedback'; ...
    };

% start with the same states as the Fixed Time trials
%   add and replace some states for reaction time behavior
reactionTimeMachine.addMultipleStates(fixedTimeStates);
reactionTimeMachine.addMultipleStates(reactionTimeStates);
taskList{'control'}{'reaction time machine'} = reactionTimeMachine;

% add a group to the loop object to accomodate the Reaction Time trials
taskLoop.addFunctionToGroupWithRank({@step, reactionTimeMachine}, reactionTime, 5);
taskLoop.mergeGroupsIntoGroup({taskName, reactionTime}, reactionTime);

% add a branch to the tree trunk to lauch a Reaction Time trial
reactionTimeTree = topsBlockTree;
reactionTimeTree.name = reactionTime;
reactionTimeTree.iterations = taskList{'control'}{'trials per block'};
reactionTimeTree.blockActionFevalable = {@runTrial, taskList, reactionTimeMachine};
taskTree.addChild(reactionTimeTree);

%% Preparation Time
preparationTimeMachine = topsStateMachine;
preparationTime = 'preparation time';
preparationTimeMachine.name = preparationTime;

% only need to modify states from Fixed Time
%   respond to input during preparation
preparationTimeStates = { ...
    'name',     'entry',        'timeout',	'input',	'exit',	'next'; ...
    'preparation',{@show,fp},	trialTime,	{@queryNextTracked,gp},{},     'feedback'; ...
    'commitment',{},            0,          {},         {},     'stimulus'; ...
    };

% start with the same states as the Fixed Time trials
%   add and replace some states for preparation time behavior
preparationTimeMachine.addMultipleStates(fixedTimeStates);
preparationTimeMachine.addMultipleStates(preparationTimeStates);
taskList{'control'}{'preparation time machine'} = preparationTimeMachine;

% add a group to the loop object to accomodate the Reaction Time trials
taskLoop.addFunctionToGroupWithRank({@step, preparationTimeMachine}, preparationTime, 5);
taskLoop.mergeGroupsIntoGroup({taskName, preparationTime}, preparationTime);

% add a branch to the tree trunk to lauch a Reaction Time trial
preparationTimeTree = topsBlockTree;
preparationTimeTree.name = preparationTime;
preparationTimeTree.iterations = taskList{'control'}{'trials per block'};
preparationTimeTree.blockActionFevalable = {@runTrial, taskList, preparationTimeMachine};
taskTree.addChild(preparationTimeTree);

%% Custom Behaviors:
% define behaviors that are unique to this task
%   the control objects above use these
%   the smaller and fewer of these, the better
function runTrial(taskList, stateMachine)
% how much of this could move into the statelist?
% clear out the game pad
gp = taskList{'input'}{'gamepad'};
gp.flushData;

% reset the targets appearance
targs = taskList{'graphics'}{'targets'};
targs.color = taskList{'graphics'}{'gray'};
targs.dotSize = taskList{'graphics'}{'neutral pixels'};

% toss a coin to pick the trial's motion direction
stim = taskList{'graphics'}{'stimulus'};
coinToss = rand(1) > .5;
if coinToss
    stim.direction = taskList{'graphics'}{'leftward'};
    taskList{'control'}{'stim direction'} = 'leftward';
else
    stim.direction = taskList{'graphics'}{'rightward'};
    taskList{'control'}{'stim direction'} = 'rightward';
end
taskList{'control'}{'coin toss'} = coinToss;
taskList{'control'}{'current choice'} = 'none';

% tell the loop to run until this state machine is done
loop = taskList{'control'}{'task loop'};
trialTime = taskList{'timing'}{'trial timeout'};
stateMachine.begin;
loop.proceedFevalable = {@isTraversing, stateMachine};
loop.runForGroup(stateMachine.name, trialTime);
WaitSecs(taskList{'timing'}{'intertrial'});

function acknowledgeLeft(taskList)
taskList{'control'}{'current choice'} = 'leftward';
targs = taskList{'graphics'}{'targets'};
targs.dotSize = taskList{'graphics'}{'leftward pixels'};


function acknowledgeRight(taskList)
taskList{'control'}{'current choice'} = 'rightward';
targs = taskList{'graphics'}{'targets'};
targs.dotSize = taskList{'graphics'}{'rightward pixels'};


function drawCorrectIncorrect(taskList)
targs = taskList{'graphics'}{'targets'};
stimDir = taskList{'control'}{'stim direction'};
choiceDir = taskList{'control'}{'current choice'};
if strcmp(stimDir, choiceDir)
    targs.color = taskList{'graphics'}{'green'};
else
    targs.color = taskList{'graphics'}{'red'};
end